package org.starfishrespect.myconsumption.android.ui;
import android.app.Activity;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.starfishrespect.myconsumption.android.R;
import org.starfishrespect.myconsumption.android.SingleInstance;
import org.starfishrespect.myconsumption.android.dao.SensorValuesDao;
import org.starfishrespect.myconsumption.android.data.FrequencyData;
import org.starfishrespect.myconsumption.android.data.SensorData;
import org.starfishrespect.myconsumption.android.data.SensorValue;
import org.starfishrespect.myconsumption.android.data.SensorValuePreProcessor;
import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.SeriesSelection;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;
import org.achartengine.util.IndexXYMap;
import org.starfishrespect.myconsumption.android.events.ColorChangedEvent;
import org.starfishrespect.myconsumption.android.events.DateChangedEvent;
import org.starfishrespect.myconsumption.android.events.FragmentsReadyEvent;
import org.starfishrespect.myconsumption.android.events.ReloadUserEvent;
import org.starfishrespect.myconsumption.android.events.UpdateMovingAverageEvent;
import org.starfishrespect.myconsumption.android.events.VisibilityChangedEvent;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.*;
import de.greenrobot.event.EventBus;
/**
* Fragment that displays the chart of ChartActivity
* S23Y (2015). Licensed under the Apache License, Version 2.0.
* Adapted from Patrick by Thibaud Ledent
*/
public class ChartViewFragment extends Fragment {
protected ChartActivity mActivity;
private static final String TAG = "ChartViewFragment";
private GraphicalView chart = null;
/* ------------------------------------------
* currentChartDataset & originalChartDataset
* ------------------------------------------
* Why keeping both of them? Because of the smoothing option given to the user, we need
* to keep the original dataset in order to recompute the moving average if the slider is
* modified. The currentChartDataset contains the values modified while the originalDataSet
* contains the one that aren't.
*/
// Dataset containing the series of point that is really displayed
private XYMultipleSeriesDataset currentChartDataset = new XYMultipleSeriesDataset();
// Dataset containing the series of point that is present in the db
private XYMultipleSeriesDataset originalChartDataset = new XYMultipleSeriesDataset();
private XYMultipleSeriesRenderer chartRenderer = new XYMultipleSeriesRenderer();
private double minimalX = Integer.MAX_VALUE, maximalX = 0, maximalY = 0;
private HashMap<String, ChartSerieRendererContainer> data;
private float touchPosX = 0, touchPosY = 0;
private int currentStart, currentDateDelay, currentValueDelay;
private TextView textViewNoData, textViewDataSensorName, textViewDataDate, textViewDataValue;
private View colorViewSelectedData;
private RelativeLayout layoutPointData;
private View refreshingView;
private LinearLayout chartLayout;
private boolean refreshing = false;
private int seekBarPosition = 0;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_chart_view, container, false);
// Register to the EventBus
EventBus.getDefault().register(this);
textViewNoData = (TextView) view.findViewById(R.id.textViewNoData);
textViewDataSensorName = (TextView) view.findViewById(R.id.textViewDataSensorName);
textViewDataDate = (TextView) view.findViewById(R.id.textViewDataDate);
textViewDataValue = (TextView) view.findViewById(R.id.textViewDataValue);
refreshingView = view.findViewById(R.id.layoutChartRefresh);
layoutPointData = (RelativeLayout) view.findViewById(R.id.layoutPointData);
colorViewSelectedData = view.findViewById(R.id.colorViewSelectedData);
chartLayout = (LinearLayout) view.findViewById(R.id.chartContainer);
return view;
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().post(new FragmentsReadyEvent(this.getClass(), true));
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = (ChartActivity) activity;
}
@Override
public void onDestroy() {
// Unregister to the EventBus
EventBus.getDefault().unregister(this);
super.onDestroy();
}
/**
* Triggered when the user wants to reload data.
* @param event A ReloadUser event
*/
public void onEvent(ReloadUserEvent event) {
// Reset and init the graph
reset();
}
/**
* Triggered when the date is changed in the spinner of ChartChoiceFragment.
* @param event A ReloadUser event
*/
public void onEvent(DateChangedEvent event) {
if (event.getDate() == null) {
return;
}
long date = event.getDate().getTime() / 1000;
showAllGraphicsWithPrecision((int) date, event.getDateDelay(), event.getValueDelay());
}
/**
* Triggered when the visibility is changed in ChartChoiceFragment.
* @param event A VisibilityChangedEvent event
*/
public void onEvent(VisibilityChangedEvent event) {
setSensorVisibility(event.getSensor(), event.getSensor().isVisible());
}
/**
* Triggered when the color is changed in ChartChoiceFragment.
* @param event A ColorChangedEvent event
*/
public void onEvent(ColorChangedEvent event) {
setSensorColor(event.getSensor(), event.getSensor().getColor());
}
/**
* Triggered when the moving average seekbar is changed in ChartChoiceFragment.
* @param event A UpdateMovingAverageEvent event
*/
public void onEvent(UpdateMovingAverageEvent event) {
seekBarPosition = event.getSeekBarPosition();
updateMovingAverage();
}
// load data from the local database to a sensor
private List<SensorValue> loadData(SensorData sensor) {
Log.d(TAG, "loading data");
List<SensorValue> values;
if (currentDateDelay == -1)
values = new SensorValuesDao(SingleInstance.getDatabaseHelper()).getValues(sensor.getSensorId(), currentStart, Integer.MAX_VALUE);
else
values = new SensorValuesDao(SingleInstance.getDatabaseHelper()).getValues(sensor.getSensorId(), currentStart, currentStart + currentDateDelay);
Log.d(TAG, "processing data");
values = SensorValuePreProcessor.fitToPrecision(values, currentValueDelay);
return values;
}
// refresh the chart for the given sensor
private void updateChartForSensor(ChartSerieRendererContainer container) {
if (container == null)
return;
XYSeriesRenderer serieRenderer = null;
// Remove former serie form dataset and renderer if present
if (container.getSerie() != null) {
currentChartDataset.removeSeries(container.getSerie());
originalChartDataset.removeSeries(container.getSerie());
}
if (container.getRenderer() != null) {
chartRenderer.removeSeriesRenderer(container.getRenderer());
serieRenderer = container.getRenderer();
}
// If the sensor is visible and the container has values
if (container.getSensor().isVisible() && container.values != null && container.values.size() > 0) {
if (serieRenderer == null) {
serieRenderer = new XYSeriesRenderer();
serieRenderer.setPointStyle(PointStyle.CIRCLE);
}
serieRenderer.setColor(container.getSensor().getColor());
final XYSeries serie = new XYSeries(container.getSensor().getSensorId());
for (SensorValue point : container.values) {
int x = point.getTimestamp();
int y = point.getValue();
if (y > maximalY) maximalY = y;
if (x > maximalX) maximalX = x;
if (x < minimalX) minimalX = x;
serie.add(x, y);
}
double lateralDelta = maximalX - minimalX;
lateralDelta /= 20;
double[] limits = {minimalX - lateralDelta, maximalX + lateralDelta, 0.0, maximalY};
chartRenderer.setPanLimits(limits);
// Add series to the datasets
originalChartDataset.addSeries(serie);
currentChartDataset.addSeries(movingAverage(
serie,
seekBarPosition));
// Add series to the renderer
chartRenderer.addSeriesRenderer(serieRenderer);
container.setRenderer(serieRenderer);
container.setSerie(serie);
if (chart == null) {
chart = ChartFactory.getLineChartView(mActivity, currentChartDataset, chartRenderer);
chartLayout.addView(chart, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
chart.setClickable(true);
chart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SeriesSelection seriesSelection = chart.getCurrentSeriesAndPoint();
if (seriesSelection != null) {
for (ChartSerieRendererContainer container : data.values()) {
if (container.getSerie() == null) {
continue;
}
if (container.values == null || container.values.size() == 0) {
continue;
}
XYSeries xyserie = currentChartDataset.getSeriesAt(seriesSelection.getSeriesIndex());
if (container.getSensor().getSensorId().equals(xyserie.getTitle())) {
int point = seriesSelection.getPointIndex();
double px = seriesSelection.getXValue();
long ts = (long) px;
int nearest = -1;
for (int i = 0; i < container.values.size(); i++) {
if (ts - container.values.get(i).getTimestamp() >= 0) {
nearest = i;
} else {
point = nearest;
break;
}
}
// correcting point, if some values on the left are not visible
layoutPointData.setVisibility(View.INVISIBLE);
textViewDataSensorName.setText(container.sensor.getName());
textViewDataDate.setText(formatDate(new Date(((long) container.values.get(point).getTimestamp()) * 1000)));
textViewDataValue.setText(container.values.get(point).getValue() + " W");
colorViewSelectedData.setBackgroundColor(container.getSensor().getColor());
float posX = touchPosX;
float posY = touchPosY;
if (posX > v.getWidth() / 2) {
posX = posX - layoutPointData.getWidth() - 10;
} else {
posX++;
}
if (posY + layoutPointData.getHeight() + 10 > v.getHeight()) {
posY = posY - layoutPointData.getHeight() - 10;
} else {
posY += 10;
}
layoutPointData.setX(posX);
layoutPointData.setY(posY);
layoutPointData.setVisibility(View.VISIBLE);
break;
}
}
} else {
layoutPointData.setVisibility(View.INVISIBLE);
}
}
});
chart.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
touchPosX = event.getX();
touchPosY = event.getY();
}
return false;
}
});
}
}
if (chart != null) {
chart.repaint();
}
if (currentChartDataset.getSeriesCount() == 0) {
textViewNoData.setVisibility(View.VISIBLE);
} else {
textViewNoData.setVisibility(View.GONE);
}
}
/**
* Formats the date to a pretty display for information popup,
* adapted with the current precision
*
* @param date the date
* @return the formatted date
*/
private String formatDate(Date date) {
String format = "";
switch (currentValueDelay) {
case FrequencyData.DELAY_DAY:
format = "EEE dd MMMM yyyy";
break;
case FrequencyData.DELAY_15MIN:
case FrequencyData.DELAY_MINUTE:
case FrequencyData.DELAY_FIVE_MINUTES:
case FrequencyData.DELAY_HOUR:
format = "EEE dd MMMM yyyy HH:mm";
break;
case FrequencyData.DELAY_MONTH:
format = "MMMM yyyy";
break;
case FrequencyData.DELAY_YEAR:
format = "yyyy";
break;
case FrequencyData.DELAY_WEEK:
Date nextWeek = new Date(date.getTime() + 6000 * 86400);
format = "EEE dd MMMM yyyy";
SimpleDateFormat formatter = new SimpleDateFormat(format);
return "Week from " + formatter.format(date) + " to " + formatter.format(nextWeek);
}
return new SimpleDateFormat(format).format(date);
}
/**
* Show all data that is marked as visible on the graph with the given interval
*
* @param start start of the range to display (timestamp)
* @param dateDelay length of the range to display
* @param valueDelay minimal time between two successive points. If time if longer,
* multiple points will be aggregated
*/
public void showAllGraphicsWithPrecision(int start, int dateDelay, int valueDelay) {
if (refreshing) {
return;
}
if (start == currentStart && dateDelay == currentDateDelay && valueDelay == currentValueDelay && dateDelay != -1) {
return;
}
try {
layoutPointData.setVisibility(View.GONE);
} catch (NullPointerException e) {
return;
}
init();
currentStart = start;
currentDateDelay = dateDelay;
currentValueDelay = valueDelay;
refreshingView.setVisibility(View.VISIBLE);
textViewNoData.setVisibility(View.GONE);
new loadLocalDataTask().execute();
}
// clears the graph
public void reset() {
Log.d(TAG, "reset");
currentStart = -1;
currentDateDelay = -1;
currentValueDelay = -1;
init();
}
// initialises the graph
private void init() {
Log.d(TAG, "init");
layoutPointData.setVisibility(View.GONE);
chartLayout.removeAllViews();
chart = null;
data = new HashMap<>();
List<SensorData> sensors = SingleInstance.getUserController().getUser().getSensors();
if (sensors.size() == 0) {
textViewNoData.setVisibility(View.VISIBLE);
textViewNoData.setText(R.string.chart_text_no_sensor);
refreshingView.setVisibility(View.GONE);
} else {
textViewNoData.setText(R.string.chart_text_no_data);
}
for (SensorData sensor : sensors) {
data.put(sensor.getSensorId(), new ChartSerieRendererContainer(sensor));
}
textViewNoData.setVisibility(View.VISIBLE);
originalChartDataset = new XYMultipleSeriesDataset();
currentChartDataset = new XYMultipleSeriesDataset();
chartRenderer = new XYMultipleSeriesRenderer();
chartRenderer.setApplyBackgroundColor(true);
chartRenderer.setBackgroundColor(Color.WHITE);
chartRenderer.setAxesColor(Color.DKGRAY);
chartRenderer.setMarginsColor(Color.WHITE);
chartRenderer.setGridColor(Color.LTGRAY);
chartRenderer.setXLabelsColor(Color.DKGRAY);
chartRenderer.setYLabelsColor(0, Color.DKGRAY);
//chartRenderer.setZoomEnabled(true);
//chartRenderer.setPanEnabled(true);
chartRenderer.setZoomEnabled(true, false);
chartRenderer.setPanEnabled(true, false);
chartRenderer.setClickEnabled(true);
chartRenderer.setShowGrid(true);
chartRenderer.setXLabelFormat(new DecimalFormat() {
@Override
public StringBuffer format(double value, StringBuffer buffer, FieldPosition position) {
Date dateFormat = new Date(((long) value) * 1000);
buffer.append(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(dateFormat));
return buffer;
}
});
chartRenderer.setShowLegend(false);
minimalX = Integer.MAX_VALUE;
maximalX = 0;
maximalY = 0;
}
/**
* Apply a moving average on the dataset based on the smoothing option chosen by the user.
* It updates the current data set displayed on screen based on the values given in the
* original data set.
*/
public void updateMovingAverage() {
if (seekBarPosition < 0)
return;
currentChartDataset.clear();
// Take into account each sensor and its associated series.
for (XYSeries series : originalChartDataset.getSeries()) {
XYSeries moving = movingAverage(series, seekBarPosition);
currentChartDataset.addSeries(moving);
}
if (chart != null) {
chart.repaint();
}
else
Log.d(TAG, "in method updateMovingAverage: chart is null while it shouldn't");
}
// Computes the moving average from a given series
private XYSeries movingAverage(XYSeries series, int N) {
if (series == null | N < 0)
return null;
if (N == 0)
return series;
Log.d(TAG, "method movingAverage called; slider position = " + N);
XYSeries moving = new XYSeries(series.getTitle());
IndexXYMap<Double, Double> iXY = (IndexXYMap<Double, Double>) series.getXYMap().clone();
Double y_n_1 = 0.0;
for (int i = 1; i < iXY.size(); i++) {
Double av;
if ((i-N) < 0)
continue;
else
av = y_n_1 + ((iXY.getYByIndex(i) - iXY.getYByIndex(i - N)) / N);
moving.add(iXY.getXByIndex(i), av);
y_n_1 = av;
}
return moving;
}
// simple container for data series
private class ChartSerieRendererContainer {
XYSeries serie;
XYSeriesRenderer renderer;
SensorData sensor;
List<SensorValue> values;
private ChartSerieRendererContainer(SensorData sensor) {
this.sensor = sensor;
}
public void setSerie(XYSeries serie) {
this.serie = serie;
}
public void setRenderer(XYSeriesRenderer renderer) {
this.renderer = renderer;
}
public XYSeries getSerie() {
return serie;
}
public XYSeriesRenderer getRenderer() {
return renderer;
}
public SensorData getSensor() {
return sensor;
}
public void setSensor(SensorData sensor) {
this.sensor = sensor;
}
}
// shows or hide a sensor from the graph
public void setSensorVisibility(SensorData sensor, boolean visible) {
layoutPointData.setVisibility(View.GONE);
ChartSerieRendererContainer container = data.get(sensor.getSensorId());
if (container == null) {
return;
}
container.getSensor().setVisible(visible);
try {
SingleInstance.getDatabaseHelper().getSensorDao().update(container.getSensor());
} catch (SQLException e) {
e.printStackTrace();
}
new loadLocalDataTask().execute(sensor);
}
// change the color of a sensor of the graph
public void setSensorColor(SensorData sensor, int color) {
layoutPointData.setVisibility(View.GONE);
ChartSerieRendererContainer container = data.get(sensor.getSensorId());
if (container == null) {
return;
}
container.getSensor().setColor(color);
try {
SingleInstance.getDatabaseHelper().getSensorDao().update(container.getSensor());
} catch (SQLException e) {
e.printStackTrace();
}
new loadLocalDataTask().execute(sensor);
}
// task to load a data serie from the database, to avoid blocking
private class loadLocalDataTask extends AsyncTask<SensorData, ChartSerieRendererContainer, Void> {
@Override
protected void onPreExecute() {
refreshing = true;
}
@Override
protected Void doInBackground(SensorData... sensors) {
if (sensors.length == 0) {
for (ChartSerieRendererContainer container : data.values()) {
if (!container.getSensor().isVisible()) {
Log.d(TAG, "INVISIBLE");
publishProgress(container);
continue;
}
if (container.values == null) {
container.values = loadData(container.sensor);
}
publishProgress(container);
}
} else {
for (SensorData sensor : sensors) {
ChartSerieRendererContainer container = data.get(sensor.getSensorId());
if (container == null) {
continue;
}
if (!container.getSensor().isVisible()) {
Log.d(TAG, "INVISIBLE");
publishProgress(container);
continue;
}
if (container.values == null) {
container.values = loadData(container.sensor);
}
publishProgress(container);
}
}
return null;
}
@Override
protected void onProgressUpdate(ChartSerieRendererContainer... values) {
for (ChartSerieRendererContainer container : values) {
updateChartForSensor(container);
}
}
@Override
protected void onPostExecute(Void aVoid) {
refreshingView.setVisibility(View.GONE);
refreshing = false;
}
}
}